package ocbn.remotelab.client;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Vector;
import javax.microedition.io.Connector;
import javax.microedition.io.SocketConnection;
import ocbn.remotelab.events.ConnStateEvent;
import ocbn.remotelab.events.Event;
import ocbn.remotelab.util.MD5;
import ocbn.remotelab.xml.XMLImport;

/**
 * This class sends and gets packets from a given server through TCP sockets,
 * processes the packet according to the protocol, and fires an Event out if an
 * MS Event packet is received.
 *  * <br><br>
 * Sending packets to the server can be done by send(String packet) method, and
 * any class that intends to receive Events should implement the
 * PacketEventListener interface and add itself to a PacketProcessor instance
 * through addListener(PacketEventListener listener).
 * @see EventListener
 * @see EventSource
 * @author c8yanche
 */
public class PacketProcessor extends EventSource {

    public static final int BB_DEFAULT = 0;
    public static final int DIRECT_TCP = 1;

    private DataInputStream fromServer;
    private DataOutputStream toServer;
    //the connection thread
    private ListeningThread conn;
    //default user logins for testing
    private static final String DEF_UNAME = "Tester";
    private static final String DEF_PRIV_KEY = "RandomTester";
    //delay for reconnection, in milliseconds
    private static final int RECONN_DELAY = 5000;
    private static final int TIME_OUT = 5000;
    private static final int RECONN_REPORT_STEP = 1000;
    private String userName, privKey, srvAddr;
    private SocketConnection socket;
    //whether the PacketProcessor is listening to the socket
    private boolean isListening;
    private boolean isAuthed;
    //whether the Listening thread is running
    private boolean isRunning;
    private String connOption;

    /**
     * Create a packet processor to a given server.
     * The packet processor establishes direct TCP connection to the server, 
     * and process responds as is in the protocol. It fires a Event when a data
     * packet receives.
     * @param address the address of the server, in the format of: <br>
     * <code> socket://&lt;server address$gt;:&lt;port number&gt;</code>
     * @throws IOException when the connection cannot be established.
     */
    PacketProcessor(String address) {
        this(address, DEF_UNAME, DEF_PRIV_KEY);
    }

    /**
     * Create a packet processor to a given server.
     * The packet processor establishes direct TCP connection to the server, 
     * and process responds as is in the protocol. It fires a Event when a data
     * packet receives.
     * @param address the address of the server, in the format of: <br>
     * <code> socket://&lt;server address$gt;:&lt;port number&gt;</code>
     * @param userName the username for login.
     * @param privKey the private key for the user.
     */
    public PacketProcessor(String address, String username, String privKey){
        this(address, username, privKey, DIRECT_TCP);
    }

    /**
     * Create a packet processor to a given server.
     * The packet processor establishes direct TCP connection to the server, 
     * and process responds as is in the protocol. It fires a Event when a data
     * packet receives.
     * @param address the address of the server, in the format of: <br>
     * <code> socket://&lt;server address$gt;:&lt;port number&gt;</code>
     * @param userName the username for login.
     * @param privKey the private key for the user.
     * @param option The method of connection. If set to BB_DEFAULT,
     * PacketProcessor connects to server using BlackBerry default setting,
     * if set to DIRECT_TCP, PacketProcessor establishes a direct TCP connection.
     * @throws IOException when the connection cannot be established.
     */
    public PacketProcessor(String address, String userName, 
            String privKey, int option) {
        this.userName = userName;
        this.privKey = privKey;
        this.srvAddr = address;
        if (option == BB_DEFAULT) {
            this.connOption = "";
        } else {
            this.connOption = ";deviceside=true";
        }
        isAuthed = false;
        isListening = false;
        isRunning = false;
    }

    /**
     * processes an incoming packet according to the protocol, and returns
     * response if required.
     * If a data packet arrived, the XML data will be converted to Event object,
     * which will be fired by calling the packetReceived() method in all its
     * listeners.
     * @param packet Incoming packet
     * @return a response packet. If no response is required, return <code>
     * null </code>.
     */
    public String process(String packet) {
        if (packet.startsWith("PING")) {
            return "ACKN";
        } else if (packet.startsWith("ACKN")) {
            return null;
            // a data packet arrived. Import from XML into Events and fire.
        } else if (packet.startsWith("DATA")) {
            //FIXME: add exception handling for broken data packets
            XMLImport xml = new XMLImport();
            Vector vecEvents = xml.importXML(packet.substring(4));
            System.out.println(vecEvents + "\n****");
            for (int i = 0; i != vecEvents.size(); i++) {
                Event e = (Event) vecEvents.elementAt(i);
                System.out.println(e + "\n *****");
                fireEvent(e);
                System.out.println("*** event fired ****");
            }
            return "ACKN";
        } else if (packet.startsWith("HKEY")) {
            return "HASH" + MD5.toMD5(packet.substring(4) + this.privKey);
        } else if (packet.startsWith("AUTH")) {
            isAuthed = true;
            fireEvent(new ConnStateEvent(
                    "CONNECTED", "Listening for Events from " + srvAddr + " ..."));
            return "ACKN";
            //login is not correct. disconnect.
        } else if (packet.startsWith("LERR")) {
            isAuthed = false;
            terminate();
            System.out.println("***LOGIN ERROR***");
            fireEvent(new ConnStateEvent(
                    "DISCONNECTED",
                    "Login error: " + packet.substring(4) +
                    " Disconnected from " + srvAddr));
            return null;
            //server error. reconnect after a while
        } else if (packet.startsWith("SERR")) {
            disconnect();
            return null;
        } else {
            //broken packets. Stop worrying and continue.
            return null;
        }
    }

    /**
     * Send a string packet to the server.
     * @param packet the packet to be sent.
     * @throws IOException when the packet cannot be sent, e.g.,
     * due to disconnection, socket does not open etc.
     */
    public void send(String packet) throws IOException {
        toServer.writeUTF(packet);
    }

    /**
     *  Start the listening thread.
     *  If the listening thread is already running, the method has no effect.
     */
    public void connect() {
        if (!isRunning) {
            isRunning = true;
            conn = new ListeningThread();
            conn.start();
        }


    }

    /**
     * Return whether the PacketProcessor is listening.
     * @return true if the PacketProcessor is listening, false if not.
     */
    public boolean isListening() {
        return isListening;
    }

    /**
     * Return whether the listening thread is running.
     * @return state of the listening thread. true if running, false if not.
     */
    public boolean isRunning() {
        return isRunning;
    }

    /**
     * Return whether login is successful on the connection.
     * @return login state. true if the login has been completed through the 
     * connection, false if not.
     */
    public boolean isAuthed() {
        return isAuthed;
    }

    /**
     * Connects to a server through direct TCP connection given server address.
     * Address in the format of "Socket://addr:port"
     * @param address the address of the server.
     */
    private void reconnect() {
        try {
            System.out.println("***Connecting***");
            fireEvent(new ConnStateEvent(
                    "RECONNECTING", "Connecting to " + srvAddr + "..."));
            socket = (SocketConnection) Connector.open(srvAddr + connOption);
            if (socket != null) {
                fromServer = socket.openDataInputStream();
                toServer = socket.openDataOutputStream();
                send("USER" + userName);
            }
            isListening = true;
            return;
        /*    fireEvent(new ConnStateEvent(
                    "CONNECTED", "Connected to " + srvAddr + ", sending login info..."));
                    
                    return;*/
            
        } catch (IOException ex) {
            System.out.println("***" + ex.getMessage() + "***");
        }
        isListening = false;


    }

    /**
     * Disconnect from the server.
     */
    private void disconnect() {

        try {
            if (socket != null) {
                fromServer.close();
                toServer.close();
                socket.close();
            }

        } catch (IOException ex) {
        }


        isAuthed = false;
        isListening = false;


    }

    /** Disconnect from the server, and terminate the listening thread
     */
    public void terminate() {
        isListening = false;
        if (isRunning) {
            if (conn.isAlive()) conn.interrupt();
            disconnect();
            fireEvent(new ConnStateEvent(
                    "DISCONNECTED", "Disconnected from " + srvAddr + "."));
        }     
        isRunning = false;
    }

    /**
     * The thread that creates i/o stream from a socket, and listens to the
     * incoming stream.
     * When connection lost, the thread autometically reconnects after 
     * RECONN_DELAY milliseconds. <BR> The thread is started by connect() method
     * and terminated by terminate() method.
     */
    private class ListeningThread extends Thread {

        private String packet, response;

        /**
         * start the listening thread.
         */
        public void run() {
            while (isRunning) {
                reconnect();
                try {
                    //a local packet specified in the client protocol, signify
                    //connection with server established.
                    System.out.println("***isListening = " + isListening);
                    while (isListening) {
                        packet = fromServer.readUTF();
                        System.out.println("*****Received: " + packet + "*****");
                        if (packet == null) {
                            isListening = false;
                            break;
                        }
                        //if response is required, sent it out.
                        if ((response = process(packet)) != null) {
                            toServer.writeUTF(response);
                        }
                    }

                    System.out.println("***Disconnected");

                } catch (IOException ex) {
                    System.out.println("Error reading socket IO stream");
                    isListening = false;

                }
                disconnect();
                if (!isRunning) break;

                System.out.println("***Nap time");
                for (int i = RECONN_DELAY; i >= 0; i -= RECONN_REPORT_STEP) {
                    try {
                        Thread.sleep(RECONN_REPORT_STEP);
                    } catch (InterruptedException ex) {
                        break;
                    }
                    fireEvent(new ConnStateEvent("RECONNECTING",
                            "Cannot connect to " + srvAddr +
                            ", retry in " + i / 1000 + " seconds..."));
                    System.out.println(i / 1000 + " seconds to work");
                }
            }
            System.out.println("***Listening thread terminated.");
        }
    }
}
